我們NestJS系列主題最後一哩路~Knex進階內容
這邊要討論幾個上一篇提到的進階主題
當然可以!首先,讓我來詳細介紹一下Knex中的Transactions機制。
在Knex中,Transactions允許你執行一系列的資料庫操作,並且保證這些操作要麼全部成功,要麼全部失敗。這在處理需要保證資料完整性的情況下非常有用,比如在新增、更新或刪除多筆資料時。
以下是在Knex中使用Transactions的步驟:
開始Transaction:
要開始一個Transaction,你需要使用
transaction
方法。這個方法會返回一個Promise,你可以在這個Promise中進行你的資料庫操作。import { transaction } from 'knex'; const executeTransaction = async () => { await knex.transaction(async (trx) => { // 在這裡進行資料庫操作 }); };
執行資料庫操作:
在Transaction內部,你可以使用
trx
物件來執行資料庫操作,比如插入、更新或刪除資料。const executeTransaction = async () => { await knex.transaction(async (trx) => { await trx('users').insert({ name: 'John Doe' }); await trx('posts').update({ title: 'New Title' }).where({ user_id: 1 }); }); };
確認Transaction:
如果所有的操作都成功完成,Transaction會自動提交。如果有任何一個操作失敗,Transaction會自動回滾,所有的操作都會被撤銷。
處理錯誤:
如果在Transaction中的任何一個操作中拋出了錯誤,Transaction會自動回滾。你可以使用try-catch來處理錯誤。
const executeTransaction = async () => { try { await knex.transaction(async (trx) => { // ... }); } catch (error) { console.error('Transaction failed:', error); } };
結束Transaction:
Transaction會在所有操作完成後自動提交或回滾,你不需要手動結束它。
這就是在Knex中使用Transactions的基本步驟。請記得在使用Transactions時要小心處理錯誤,以確保資料的完整性。
如果你有任何其他的問題或需要進一步的說明,請隨時告訴我!
範例中的knex
就是替換成我們上個章節處理好的連線 this.knexService.db
的那個東西
因為是新的對話,所以ChatGPT不清楚我們的設置,以下就自行替換連線語法
看了以上內容,我們現在不知道使用時機以及他的運作邏輯是什麼、如何達成他所說的回滾
繼續追問!
Transactions通常用在需要保證一系列資料庫操作要麼全部成功,要麼全部失敗的情況下。以下是一些常見的使用情境:
- 保證資料完整性:當你需要在多個表中進行操作,並且要保證這些操作要麼全部成功,要麼全部失敗時,可以使用Transactions。比如在新增一筆訂單時,你可能需要同時更新庫存和記錄交易紀錄。
- 防止競態條件:當多個使用者同時對同一筆資料進行操作時,可能會發生競態條件(race condition)的情況。使用Transactions可以確保在一個Transaction中的操作是原子的,不會被其他使用者干擾。
- 批量操作:當你需要執行一系列相關的資料庫操作時,可以使用Transactions來保證這些操作的完整性。
現在讓我來詳細說明Knex對於Transactions功能的實作原理:
在Knex中,當你開始一個Transaction時,它會在資料庫中啟動一個隔離層,稱為Transaction隔離。這個隔離層會追蹤所有在Transaction內進行的資料庫操作,並且在Transaction提交或回滾時確保這些操作的完整性。
Knex使用資料庫引擎(如MySQL、PostgreSQL等)提供的Transaction特性來實現這個功能。具體來說,它使用了資料庫引擎的
BEGIN TRANSACTION
、COMMIT
和ROLLBACK
等指令來控制Transaction的開始、提交和回滾。當你在Transaction中執行操作時,Knex會將這些操作包裝在一個事務中,並且在事務結束時使用
COMMIT
指令提交,或者在發生錯誤時使用ROLLBACK
指令回滾。這樣,就保證了在一個Transaction中的所有操作要麼全部成功,要麼全部失敗。
希望這樣的解釋對你有幫助!如果你有任何其他的問題,請隨時告訴我。
ChatGPT所說的情況,我自己在工作上也都有遇到
第一點&第三點比較容易懂,多個或單個表格中做了一系列的更改操作
為了防範中途出現問題,造成有些指令有執行,而有些沒有
所以使用一個Transaction ,確保全部執行,全是全部不執行
這邊針對第二點稍微說明一下
在我工作上,有一個資料表中會有一個「任務清單」,裡面的資料列會代表著一件一件的任務
清單內有欄位表示ID、目前狀態、正在執行這個任務的使用者ID(可能為空)
多個使用者都會頻繁地select這個任務清單,
當有任務需要處理時,就按下按鈕發送請求,希望從任務清單中分配一件任務到自己手上
所以發送請求之後,伺服器做的事情是這樣
這兩個動作的query是分開丟給SQL的
就會導致也許有多個使用者在執行步驟一:找一筆待處理任務的ID時
取得一樣的ID,接著第二步就會有人變更成功,有人變更失敗
同樣的情況可能會發生在購買庫存貨、訂票,或… YOASOBI演唱會售票(X)
為什麼Transaction 可以防止這樣的事情發生呢?
我們請ChatGPT解釋說明
當你使用Transactions來防止競態條件時,你通常是在多個使用者或多個執行緒之間進行操作,以確保一個操作完成後,下一個操作才能開始。這是非常重要的,因為如果多個使用者同時對資料進行操作,可能會導致不一致的資料狀態。
以下是一個情境示範,假設你有一個資料庫表格用於儲存產品庫存,並且多個使用者想要同時更新這個庫存:
import { transaction } from 'knex'; const updateProductStock = async (productId, quantityToDeduct) => { try { // 開始一個Transaction await knex.transaction(async (trx) => { // 先查詢當前庫存數量 const currentStock = await trx('products').select('stock').where('id', productId).first(); if (currentStock) { // 檢查是否庫存足夠 if (currentStock.stock >= quantityToDeduct) { // 扣減庫存 await trx('products').where('id', productId).decrement('stock', quantityToDeduct); } else { throw new Error('庫存不足'); } } else { throw new Error('找不到產品'); } }); console.log('庫存更新成功'); } catch (error) { console.error('庫存更新失敗:', error.message); } };
在這個示範中,我們使用Transactions來保護對庫存的更新操作。當一個使用者想要扣減庫存時,我們首先查詢當前庫存,然後檢查是否庫存足夠。如果庫存足夠,我們扣減庫存,然後在Transaction中提交這個操作。
這樣,當多個使用者同時嘗試更新庫存時,Transactions確保這些操作是原子的,不會交錯進行,從而防止競態條件的發生。如果庫存不足或找不到產品,Transaction會回滾,確保資料的完整性。
這是一個簡單的情境示範,但Transactions可以在更複雜的應用中用於處理競態條件,確保資料的一致性。
Transactions在防止兩個請求同時完成「檢查是否庫存足夠」的步驟的過程中,利用了資料庫引擎的隔離層(isolation level)和鎖定機制。
具體來說,當你在一個Transaction中進行資料庫操作時,資料庫會將相關的資料行或表格進行鎖定,以防止其他使用者對這些資料進行操作。這樣可以確保在這個Transaction中進行的操作不會被其他使用者干擾。
在我們的情境示範中,當一個使用者嘗試扣減庫存時,首先會進行一個查詢操作,以獲取當前的庫存數量。這個查詢操作會在Transaction內部進行,而且在這個Transaction執行期間,相關的資料行或表格會被鎖定。
這意味著在這個Transaction執行期間,其他使用者如果也想要進行與這筆庫存相關的操作(比如更新、扣減),他們將會被阻止,直到這個Transaction完成。
這樣,就保證了在這個Transaction中的「檢查是否庫存足夠」步驟能夠獲得正確的庫存數量,而不會受到其他使用者的干擾。
總結來說,Transactions利用了資料庫引擎的隔離層和鎖定機制,確保在一個Transaction中的操作是原子的,不會被其他使用者干擾,從而防止競態條件的發生。
所以可以想像成是在櫃檯點餐,一個人確認完庫存,並點完餐之後,才會輪到下一個人確認庫存
在Transaction內的操作,都必須完成之後,才會做下一個Transaction
接下來一起來了解一下ChatGPT所說的模組化查詢是什麼
這個名詞我也第一次聽到,所以也是上一篇才臨時加入大綱內的
當然可以!讓我來為你詳細介紹如何在Nest專案中使用Knex進行模組化查詢。
首先,確保你已經在Nest專案中安裝了Knex和相應的資料庫驅動程式(比如
pg
、mysql2
等)。
安裝 Knex:
使用以下指令在你的Nest專案中安裝Knex:
npm install knex
建立 Knex 連線:
在Nest專案中,你可以在
src
目錄下建立一個knex
資料夾,並在其中建立一個knex.ts
文件來配置Knex連線。// src/knex/knex.ts import * as knex from 'knex'; const config = { client: 'pg', // 根據你使用的資料庫選擇相應的client connection: { host: 'localhost', user: 'your_username', password: 'your_password', database: 'your_database', }, }; const knexInstance = knex(config); export default knexInstance;
建立模組化查詢:
在Nest中,你可以根據你的需求建立不同的模組,每個模組都可以包含相應的Knex查詢。
例如,如果你想要建立一個與使用者相關的模組,可以進行如下操作:
創建一個名為
users
的資料夾,在其中包含users.module.ts
和users.service.ts
文件。在
users.service.ts
中,你可以使用之前建立的Knex連線,並實現相關的查詢操作:// src/users/users.service.ts import { Injectable } from '@nestjs/common'; import knex from '../knex/knex'; @Injectable() export class UsersService { async getAllUsers() { return await knex('users').select('*'); } async getUserById(id: number) { return await knex('users').where('id', id).first(); } async createUser(user: any) { return await knex('users').insert(user).returning('*'); } async updateUser(id: number, user: any) { return await knex('users').where('id', id).update(user).returning('*'); } async deleteUser(id: number) { return await knex('users').where('id', id).del(); } }
將模組加入 Nest 中:
在
users.module.ts
文件中,將UsersService
加入到Nest模組中。// src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersService } from './users.service'; @Module({ providers: [UsersService], exports: [UsersService], }) export class UsersModule {}
在 Controller 中使用模組:
最後,在你的Nest Controller 中使用剛剛建立的
UsersService
,以處理相應的HTTP請求。// src/users/users.controller.ts import { Controller, Get, Param } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() getAllUsers() { return this.usersService.getAllUsers(); } @Get(':id') getUserById(@Param('id') id: string) { return this.usersService.getUserById(Number(id)); } }
這樣,你就建立了一個模組化的Knex查詢在Nest專案中。你可以根據需要在
UsersService
中添加其他查詢,並在Controller中使用它們來處理相應的請求。希望這個教學對你有幫助!如果你有任何其他的問題,請隨時告訴我。
所以模組化有點像是先把一個Query指令存放在一個function中
概念我覺得有點像是資料庫的預儲程序那樣,將指令包裝起來以便重複使用
後續就是直接呼叫這個預先定義好的function,來進行query
來和ChatGPT確認一下什麼時候會需要「模組化查詢」,目的、優缺點各別是什麼
使用模組化查詢是為了更有效地組織和管理資料庫操作,尤其是在大型或複雜的應用程序中。以下是選擇使用模組化查詢的情況、目的以及其優點和缺點:
情況:
- 大型應用程序:當你的應用程序變得複雜,包含許多不同的資料庫操作時,模組化查詢可以幫助你組織和管理這些操作。
- 多個模塊:如果你的應用程序分為多個模塊,每個模塊需要進行不同的資料庫操作,你可以為每個模塊創建獨立的模組化查詢。
- 提高代碼重用性:模組化查詢可以被多個部分重複使用,減少代碼重複並提高代碼的可維護性。
- 降低耦合度:模組化查詢使得你可以將資料庫操作從控制器和服務中分離出來,降低它們之間的耦合度。
目的:
- 組織和管理資料庫操作:模組化查詢可以幫助你將相關的資料庫操作組織在一起,使代碼更易於維護。
- 提高代碼可讀性:通過將相關操作分組在一個模組中,可以使代碼更易讀和理解。
- 促進代碼重用:模組化查詢可以在應用程序的不同部分之間共享,提高代碼的重用性。
優點:
- 組織性:模組化查詢可以幫助你清晰地組織和管理資料庫操作,使代碼結構更加清晰。
- 可維護性:通過將相關操作放在一個模組中,可以更容易地進行修改、維護和測試。
- 代碼重用:可以在不同的部分中重複使用模組化查詢,減少重複代碼的數量。
缺點:
- 過度抽象:如果過度使用模組化查詢,可能會導致過度抽象和過度分層,使得代碼變得難以理解。
- 性能影響:過多的模組化查詢可能會增加函數調用的次數,導致較小的性能影響。
綜合來說,模組化查詢是一種有助於組織和管理資料庫操作的有效方法,特別適用於大型或複雜的應用程序。然而,需要注意避免過度抽象和過度使用,以保持代碼的清晰度和可讀性。
這邊插播一個上一篇忘記討論的
上一篇提到如果要使用像是mysql的 now()
這樣的內建函數
必須使用.raw()
來寫原始SQL語法語句,然後執行
但會用到 now()
的機率其實不算低
來問一下ChatGPT有沒有其他方式可以達成
除了使用
.raw()
方法之外,Knex還提供了另一種方法來使用SQL內建函數,那就是使用.fn()
方法。使用
.fn()
方法
.fn()
方法允許你使用SQL內建函數,並且提供了更清晰的方式來將它們整合到你的查詢中。例如,如果你想在Knex查詢中使用SQL的
now()
函數來取得當前的時間戳記,你可以這樣做:import knex from 'knex'; const currentTime = knex.fn.now(); knex('table_name') .update({ updated_at: currentTime }) .where('id', '=', 1) .then((result) => { console.log(result); });
在這個例子中,我們使用了
.fn()
方法來呼叫SQL的now()
函數,並將其存儲在currentTime
變數中。接著,我們將這個變數用於Knex的update查詢中,以更新updated_at
欄位。
.fn()
方法提供了一種更直觀的方式來使用SQL內建函數,而不需要直接使用原始的SQL語句。總結來說,除了使用
.raw()
方法之外,你還可以使用.fn()
方法來整合SQL內建函數到你的Knex查詢中,這樣可以使你的代碼更具可讀性和維護性。
當使用Knex進行SQL查詢時,
.fn()
方法用於整合SQL內建函數到你的查詢中。這個方法提供了一種較為清晰和可讀性較高的方式來使用這些函數,而不需要撰寫原始的SQL語句。以下是關於.fn()
的詳細教學,以及與.raw()
的比較:使用
.fn()
方法:
引入 Knex:
首先,確保你已經引入Knex,並且建立了Knex的實例。
const knex = require('knex')({ client: 'mysql', // 選擇你使用的資料庫 connection: { host: 'localhost', user: 'your_username', password: 'your_password', database: 'your_database', }, });
使用
.fn()
方法:使用
.fn()
方法可以呼叫SQL內建函數,並將其整合到你的Knex查詢中。const currentTimestamp = knex.fn.now(); knex('your_table') .select(currentTimestamp) .then((result) => { console.log(result); });
在這個例子中,我們使用
.fn.now()
來呼叫SQL的now()
函數,然後將其整合到select查詢中,以取得當前的時間戳記。不同於
.raw()
方法:
.fn()
更安全:使用
.fn()
方法可以幫助防止SQL注入攻擊,因為Knex會處理函數的參數,確保它們不會被視為SQL代碼的一部分。這使你的應用程序更加安全。更可讀性:
.fn()
提供了一種更具可讀性的方式來使用SQL內建函數,而不需要在代碼中嵌入原始的SQL語句。這使代碼更易於理解和維護。跨資料庫相容性:
使用
.fn()
方法可以提高代碼的跨資料庫相容性,因為不同的資料庫可能具有不同的函數和語法。.fn()
會自動根據所使用的資料庫引擎生成正確的SQL語句。總之,
.fn()
方法提供了一個更安全、可讀性較高、跨資料庫相容的方式來整合SQL內建函數到你的Knex查詢中,相較於.raw()
方法更受推薦。當你需要使用SQL內建函數時,.fn()
通常是更好的選擇。
接下來探討的是migration
在上一篇中,ChatGPT有提到knex的其中一個功能是可以做migration
這個是什麼呢?作用和目的是什麼,我們一起來問ChatGPT
Migration 是一種軟體開發實踐,它用於管理和跟蹤資料庫模式的變化。在開發過程中,隨著需求的變化或新功能的添加,資料庫的結構可能會需要進行修改,這時就需要使用 migration 來管理這些變化。
以下是 migration 的詳細介紹:
作用:
保持資料庫的版本控制:
Migration 允許你在專案的不同階段保持資料庫的版本控制。每個 migration 檔案代表了一個特定版本的資料庫結構,這樣你可以隨時回到過去的版本或前進到新的版本。
跟蹤資料庫模式的變化:
通過使用 migration,你可以跟蹤資料庫模式的變化,包括新增表格、刪除表格、修改欄位等。這使得團隊成員可以了解資料庫結構的演變。
在多個環境中保持一致性:
在開發、測試和生產環境中,資料庫結構可能會有所不同。透過使用 migration,你可以確保在不同環境中的資料庫結構保持一致。
Migration 流程:
創建 Migration 檔案:
每次你需要修改資料庫結構時,你會創建一個新的 migration 檔案,並在其中定義這些變化。
執行 Migration:
使用 migration 工具(如Knex CLI)執行這些 migration 檔案,將變化應用到資料庫中。
記錄 Migration 狀態:
當 migration 成功應用後,會在資料庫中記錄相應的狀態,標記哪些 migration 已經被應用。
回滾 Migration:
如果需要回到之前的版本,可以使用 migration 工具回滾到先前的狀態,並還原資料庫結構。
優點:
可追蹤和可管理:
透過 migration,你可以清晰地追蹤資料庫結構的變化,使其可管理。
版本控制:
可以使用版本控制系統(如Git)來管理 migration 檔案,保證在不同版本之間的一致性。
多環境適用:
可以在不同環境中(開發、測試、生產)保持資料庫結構的一致性。
缺點:
學習成本:
需要一些時間來學習和習慣使用 migration 工具,特別是對於初學者來說可能會有一定的學習曲線。
可能需要手動處理特殊情況:
在某些情況下,可能需要手動處理一些特殊的資料庫變化,而不是依賴 migration 工具自動處理。
總的來說,使用 migration 是一個良好的實踐,可以幫助你管理和跟蹤資料庫結構的變化,保持資料庫的一致性和可追蹤性。
這麼說起來,有時在工作時,會有測試環境的資料庫、正式資料庫等
有些時候在測試資料庫追加或更改了一些欄位
但上到正式之後因為沒有進行同步,又沒有好好的開出release分支測試
導致上線後服務掛掉,要兩邊對照,到底兩邊資料庫的差異是什麼
更改對應資料欄位之後服務才正常
如果資料庫的schema也能得到版本控制,對於管理上會更穩定
了解用途與優點後,來看看這個機制實際上是怎麼運作的
Migration的運作原理涉及到了幾個關鍵的概念和步驟,讓我來為你進行更詳細的解析與說明:
1. Migration Table:
在Migration的運作中,通常會建立一個特殊的資料表(通常稱為
migrations
或類似的名稱)來記錄已經被應用的migration的狀態。這個表通常包含以下欄位:
id
: 用來唯一識別每個migration。name
: migration的名稱或標識。batch
: migration所屬的批次,通常用來記錄這個migration是在哪一個階段被應用的。2. Migration Files:
Migration通常以檔案的形式存在,每個migration都是一個獨立的檔案,這些檔案被組織在一個特定的目錄中。
這些檔案通常包含以下內容:
up
: 定義了這個migration要執行的升級操作,也就是要將資料庫結構升級到這個版本。down
: 定義了這個migration的回滾操作,也就是要將資料庫結構還原到這個版本之前的狀態。3. Migration操作流程:
執行新的Migration:
當你執行一個新的Migration時,migration工具會讀取這個檔案,並將其中的升級操作(
up
)應用到資料庫中,同時記錄下這個migration已經被應用。記錄Migration狀態:
在資料庫的
migrations
表中記錄下這個migration的狀態,包括它的名稱、批次等信息,這樣可以在未來追蹤和管理這個migration。回滾Migration:
如果需要回滾到先前的版本,migration工具會讀取相應的migration檔案,並執行其中的回滾操作(
down
),同時在migrations
表中更新相關的狀態。4. 執行順序和版本控制:
通常,migration工具會根據
migrations
表中的狀態,確定要執行哪些migration以及它們的執行順序。這樣可以保證資料庫結構的演進是有序的,並且可以隨時回滾到先前的版本。5. 特殊情況的處理:
在某些情況下,可能需要手動處理一些特殊的資料庫變化,這時可以在migration的回滾操作(
down
)中進行相應的處理。總的來說,Migration通過管理和追蹤資料庫模式的變化,提供了一個有效的方式來保持資料庫的版本控制,並確保在不同版本之間的一致性。它涉及到創建migration檔案、執行升級和回滾操作,以及在
migrations
表中記錄相應的狀態。這使得開發團隊可以輕鬆地追蹤和管理資料庫結構的變化,並保證在不同環境中的一致性。
接下來直接來上教學吧!
當你在NestJS專案中已經安裝好Knex和mysql2時,接下來我們會進行以下步驟:
- 創建
knexfile.ts
- 創建 Migration
步驟 1: 創建
knexfile.ts
首先,在你的NestJS專案根目錄下,創建一個名為
knexfile.ts
的檔案。這個檔案將包含Knex的配置信息。// knexfile.ts import { Knex } from 'knex'; const config: Knex.Config = { client: 'mysql2', // 選擇你使用的資料庫 connection: { host: 'localhost', user: 'your_username', password: 'your_password', database: 'your_database', }, migrations: { directory: __dirname + '/src/migrations', // 告訴Knex migration檔案的存放位置 tableName: 'knex_migrations', // 設定migration記錄的資料表名稱 }, }; export default config;
在這個例子中,我們使用了MySQL2作為資料庫客戶端,並提供了相應的連線資訊。同時,我們也配置了migration相關的設定,告訴Knex migration檔案的存放位置和記錄migration狀態的資料表名稱。
步驟 2: 創建 Migration
接下來,我們將創建一個migration來示範如何進行資料庫模式的變化。
在終端中執行以下指令以創建一個migration:
npx knex migrate:make create_users_table
這會在
src/migrations
目錄下創建一個新的migration檔案,檔案名稱會包含timestamp和migration名稱(create_users_table
)。打開新創建的migration檔案,並在
up
方法中定義創建users
表格的操作,並在down
方法中定義回滾這個操作的邏輯。// 例: 20231117123456_create_users_table.ts import { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.createTable('users', (table) => { table.increments('id').primary(); table.string('name'); table.string('email').unique(); table.timestamps(true, true); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.dropTable('users'); }
最後,執行以下指令來應用這個migration:
npx knex migrate:latest
這會將
users
表格創建到你的資料庫中。現在,你已經完成了NestJS專案搭配Knex和MySQL進行migration的設定和操作。當你需要進行資料庫結構變更時,只需創建一個新的migration並執行即可。
照著做之後,跑出了Batch 1 run: 1 migration
的訊息
基本上就是初次的設定成功了
然後來看看資料庫這邊,除了之前練習時就有的表格之外,多出了他所指定的users表格
以及另外兩張管理migration用的表:knex_migrations
和knex_migrations_lock
也是一開始在講解詳細流程的時候,ChatGPT說的Migration Table
請ChatGPT再細細講一下這兩個表格的作用
當你執行
npx knex migrate:latest
時,Knex 會自動創建兩個特殊的資料表:knex_migrations
和knex_migrations_lock
,這兩個表格是用來管理 Migration 的狀態的。1.
knex_migrations
資料表:
knex_migrations
是一個用來記錄已經被應用的 migrations 的表格。這個表格包含了以下欄位:
id
: 一個唯一的識別符,通常是一個自增的數字。name
: Migration 的名稱或標識,通常是對應 migration 檔案的名稱。batch
: Migration 所屬的批次,用來標識這個 migration 是在哪一個階段被應用的。
knex_migrations
會隨著每次執行 migration 指令而被更新,記錄了已經被應用的 migrations。2.
knex_migrations_lock
資料表:
knex_migrations_lock
是用來實現 migration 鎖定機制的表格。當你執行npx knex migrate:latest
或其他 migration 相關的指令時,Knex 會先對這個表格進行操作,以確保同一時間只有一個 migration 在運行。這個表格只包含一個欄位:
is_locked
: 一個布林值,用來表示 migration 是否被鎖定。當一個 migration 開始執行時,會將此欄位設為true
,執行完畢後再設為false
。透過這個表格,Knex 可以確保同一時間只有一個 migration 在運行,避免了可能的併發問題。
總的來說,
knex_migrations
和knex_migrations_lock
這兩個表格是 Knex 用來管理 migration 狀態和實現鎖定機制的關鍵元素。它們確保了 migration 的執行是有序的,並避免了可能的併發問題。
搭個流程圖!
我可以為你提供文字描述,但是目前無法直接以Mermaid語法繪製流程圖。請參考以下描述:
- 執行
npx knex migrate:latest
指令。- Knex 首先檢查
knex_migrations_lock
資料表,看是否有鎖定。- 如果
knex_migrations_lock
中的is_locked
欄位為true
,表示已經有其他 migration 正在執行,Knex 會等待直到該 migration 完成。- 如果
knex_migrations_lock
中的is_locked
欄位為false
,Knex 會將其設為true
,表示這個 migration 已經開始執行。- Knex 接著開始執行 migration,依次應用每一個尚未被應用的 migration。
- 對於每一個 migration,Knex 會先在
knex_migrations
資料表中記錄相關的狀態,包括 migration 的名稱和批次。- 接著,Knex 會執行 migration 中的
up
方法,將資料庫結構升級到對應版本。- 完成 migration 後,Knex 會在
knex_migrations
中更新相關的狀態,標識這個 migration 已經被應用。- 最後,Knex 會將
knex_migrations_lock
中的is_locked
欄位設為false
,表示這個 migration 完成執行。這個流程保證了在同一時間只有一個 migration 在執行,並且 migration 的執行是有序的。如果在執行期間有其他 migration 同時進行,Knex 會等待直到前一個 migration 完成後再執行下一個。這樣可以避免可能的併發問題。
鍥而不捨,再來一次
抱歉,我之前的回答有誤,事實上我可以使用Mermaid語法繪製流程圖,以下是
npx knex migrate:latest
執行時knex_migrations
和knex_migrations_lock
的運作原理流程圖:graph TD subgraph User A[執行 'npx knex migrate:latest'] end subgraph Knex_Migrations_Lock B[檢查 'knex_migrations_lock' 中的 'is_locked'] C[設定 'is_locked' 為 true] D[等待] E[設定 'is_locked' 為 false] end subgraph Knex_Migrations F[記錄 migration 的狀態] G[執行 migration 的 'up' 方法] H[更新 migration 的狀態] end A --> B B --|is_locked=false|--> C B --|is_locked=true|--> D C --> F F --> G G --> H H --> E D --> E
上述流程圖說明了執行
npx knex migrate:latest
時,Knex 如何操作knex_migrations
和knex_migrations_lock
這兩個表格。希望這能幫助你更好地理解運作原理。
今天如果我還要針對表格去做變動,例如上面的users表格要追加phone欄位的話?
來看看使用migrations的機制要怎麼做
要追加一個
phone
欄位到users
表格中,你可以創建一個新的 migration 來實現這個變化。以下是具體的步驟:步驟 1: 創建 Migration
在終端中執行以下指令以創建一個新的 migration:
npx knex migrate:make add_phone_to_users_table
這會在
src/migrations
目錄下創建一個新的 migration 檔案,檔案名稱會包含 timestamp 和 migration 名稱(add_phone_to_users_table
)。步驟 2: 編輯 Migration 檔案
打開新創建的 migration 檔案,並在
up
方法中定義添加phone
欄位的操作,並在down
方法中定義回滾這個操作的邏輯。// 例: 20231118123456_add_phone_to_users_table.ts import { Knex } from 'knex'; export async function up(knex: Knex): Promise<void> { return knex.schema.table('users', (table) => { table.string('phone'); }); } export async function down(knex: Knex): Promise<void> { return knex.schema.table('users', (table) => { table.dropColumn('phone'); }); }
步驟 3: 執行 Migration
最後,執行以下指令來應用這個新的 migration:
npx knex migrate:latest
這會將
phone
欄位添加到你的users
表格中。現在,你的
users
表格應該包含了一個新的phone
欄位。請注意,這個 migration 也會被記錄在knex_migrations
中,以便在未來進行回滾或追蹤變化。
執行完之後,knex_migrations
的表格追加了一行,紀錄了已經有執行過了
如果要再度執行 npx knex migrate:latest
,就會顯示已經到最新了!
接下來我們要試看看,用現有的migrations檔案,來更新到新的DB中
如果你想在一個新的資料庫(例如名為
newDB
)上執行相同的 migrations,你需要做以下幾個步驟:步驟 1: 創建新的資料庫
首先,確保你已經在你的資料庫伺服器上創建了一個名為
newDB
的新資料庫。步驟 2: 修改
knexfile.ts
在你的 NestJS 專案中,打開
knexfile.ts
檔案,修改connection
部分以指向新的資料庫newDB
。// knexfile.ts import { Knex } from 'knex'; const config: Knex.Config = { client: 'mysql2', connection: { host: 'localhost', user: 'your_username', password: 'your_password', database: 'newDB', // 修改為新的資料庫名稱 }, migrations: { directory: __dirname + '/src/migrations', tableName: 'knex_migrations', }, }; export default config;
步驟 3: 執行 Migration
現在,你可以執行以下指令來應用 migrations 到新的資料庫
newDB
上:npx knex migrate:latest
這將會在
newDB
上創建相應的表格和結構。請確保你在運行指令之前已經在資料庫伺服器上創建了
newDB
資料庫。這樣你就可以在新的資料庫上執行相同的 migrations 了。
OK,所以首先加一個新的DB (或是本來就有其他目的資料庫)
然後更改一下 knexfile.ts
的連線database名稱
執行指令之後
因為會依序執行目前的兩個migrations的「up」的內容
就可以看到newDB上有一樣的表格和記錄了!
上一篇也有說了,knex不一定是處理資料庫的首選
他的缺點我個人是覺得學習資源比較少(雖然可以用ChatGPT補足)
請ChatGPT來介紹其他可以做migration 的工具與優缺點比較!
除了 Knex 之外,還有一些其他 Node.js 工具套件可以處理 Migration。以下是其中幾個常用的:
- Sequelize:
- 特色:
- Sequelize 是一個支援多種資料庫的 ORM (Object-Relational Mapping) 庫,可以簡化資料庫操作。
- 提供了內建的 Migration 功能,允許你通過定義 Migration 文件來進行資料庫結構的變化。
- 支援多種資料庫類型,包括 MySQL、PostgreSQL、SQLite 等。
- 優點:
- 集成了 ORM 功能,使得資料庫操作更加便捷和直觀。
- 支援多種資料庫,可以輕鬆切換不同的資料庫類型。
- 缺點:
- 學習曲線較陡,尤其對於初學者來說可能需要一些時間適應。
- 在一些特定情況下,可能需要手動處理一些複雜的資料庫操作。
- TypeORM:
- 特色:
- TypeORM 是一個 TypeScript 環境下的 ORM 庫,提供了類似於 Sequelize 的功能,但更注重於 TypeScript 的特性和類型安全。
- 支援多種資料庫,包括 MySQL、PostgreSQL、SQLite 等。
- 具有內建的 Migration 功能,可以通過指令來創建和執行 Migration。
- 優點:
- 提供了 TypeScript 支援,具有嚴格的類型檢查,使得開發過程更加安全和穩定。
- 支援多種資料庫,並提供了豐富的功能和選項。
- 缺點:
- 學習曲線較陡,尤其對於初學者來說可能需要一些時間適應。
- Bookshelf.js:
- 特色:
- Bookshelf.js 是一個資料庫查詢和ORM庫,建立在Knex之上,對於簡化資料庫操作非常有用。
- 提供了簡單的Model定義和關聯,方便操作資料庫。
- 優點:
- 簡單易用,對於小型項目或需要輕量級 ORM 功能的場景非常適用。
- 具有良好的文件和社群支援。
- 缺點:
- 功能相對於 Sequelize 或 TypeORM 來說較為簡單,可能不適用於複雜的資料庫操作。
- Objection.js:
- 特色:
- Objection.js 是基於 Knex 的資料庫查詢和ORM庫,提供了類似於 Bookshelf.js 的功能,但具有更多的擴展性和功能。
- 優點:
- 可以方便地定義模型和關聯,支援更複雜的查詢和操作。
- 有良好的文件和活躍的社群支援。
- 缺點:
- 學習曲線可能較陡,尤其對於初學者來說可能需要一些時間適應。
選擇使用哪個工具套件取決於你的專案需求、團隊熟悉度以及個人偏好。每個工具套件都有其獨特的特色和優缺點,請根據自己的需求做出適當的選擇。
在這次的對話中,我們討論了關於資料庫 Migration 的相關主題。以下是一些重點摘要:
- Knex.js 是一個強大的 Node.js 資料庫查詢和 Migration 工具,它提供了一種簡單且直觀的方式來管理資料庫結構的變化。
- Migration 是用來管理和追蹤資料庫結構變化的過程,它包括了創建 Migration 檔案、執行升級和回滾操作,以及在
knex_migrations
表中記錄相應的狀態。- Transactions 可以保證一系列的資料庫操作要麼全部成功要麼全部失敗,這對於確保資料庫的一致性非常重要,特別是在處理敏感數據或者涉及多個操作的情況下。
- 模組化查詢可以幫助你維護複雜的資料庫操作,它允許你將多個查詢劃分成小的模組,提高了代碼的可讀性和可維護性。
- 內建函數(例如 now())可以在 Knex 中使用
.raw()
或.fn()
進行操作,這允許你在 SQL 查詢中使用資料庫特定的內建函數。此外,我們還探討了一些其他的 Node.js 工具套件,如 Sequelize、TypeORM、Bookshelf.js 和 Objection.js,它們也可以用於處理 Migration。
總的來說,選擇適合自己專案的 Migration 工具是根據專案需求、團隊熟悉度和個人偏好來做出的重要決策。希望這些訊息對你在開發前端工程師的過程中有所幫助!如果你還有其他問題,請隨時向我詢問。
本次對話記錄在這邊:
https://chat.openai.com/share/9ae3bb71-c5c5-4fca-aaad-ed62080a0946
嚴格上來說,Migration 完全可以切出去變成獨立的一篇
本來是想介紹knex的進階功能,主要要講transaction
我原先對於Migration 也不是很熟,只有大概知道有這個功能,但沒有實際操作過
不過這樣走過一次,也算是有知道要怎麼操作了!
雖然語法還不會!但我們有ChatGPT沒問題的(?)
那到這邊為止,和NestJS相關的主題就到這邊告一段落
這個系列文從Day1到Day50都是日更,雖然開賽前有事先準備五篇草稿
不過從開賽以來,仍然維持每天 0.5~1.5篇左右的進度,積存草稿在1~5篇之間遊走
到今天為止就沒有任何儲存文章了XD
總之!「點亮前後端技能樹」的部分這邊先告一個小段落
接下來會是「ChatGPT幫我完成工作!」的小系列
主要是講述我自己在ChatGPT推出了之後,
常常請ChatGPT如何幫我完成工作,或是解決工作上的問題
謝謝各位的訂閱與閱讀,和我一起點亮技能樹
我是目標讓技能樹亮晶晶的前端工程師一宵三筵,我們也許明天見!